// SPDX-FileCopyrightText: © 2022 ChiselStrike <info@chiselstrike.com>

//! Filter splitting optimization.
//!
//! The `predicate` in a `filter(predicate)` call can have expressions or
//! side-effects that prevent the compiler from generating a query expression,
//! because they cannot be transformed to SQL by the ChiselStrike runtime.
//!
//! This module implements a filter splitting optimization that allows the following
//! piece of code, for example:
//!
//! ```typescript
//! Person.cursor().filter(person => person.age > 40 && fetch("https://example.com"))
//! ```
//!
//! can be transformed into the semantic equivalent of:
//!
//! ```typescript
//! Person.cursor()
//!       .filter(person => person.age > 40)
//!       .filter(person => fetch("https://example.com"))
//! ```
//!
//! which then allows the compiler to transform the first `filter()` call's
//! predicate function into a query expression.

use crate::query::Expr as QExpr;
use crate::transforms::utils::convert_filter_expr;
use swc_ecmascript::ast::{ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, Expr, Stmt};

/// Splits an expression if needed and then converts it to a query expression.
///
/// The function returns a tuple of `Option<(Expr, QExpr)>` that represents the
/// converted query in AST and IR form and `Option<Expr>` that represents the
/// unconvertable post expression generated by the split.
pub fn split_and_convert_expr(expr: &Expr) -> (Option<(Expr, QExpr)>, Option<Expr>) {
    if let Some(qexpr) = convert_filter_expr(expr) {
        return (Some((expr.clone(), qexpr)), None);
    }
    match expr {
        Expr::Bin(BinExpr {
            op: BinaryOp::LogicalAnd,
            left,
            right,
            span: _,
        }) => {
            if let Some(qleft) = convert_filter_expr(left) {
                (Some((*left.to_owned(), qleft)), Some(*right.to_owned()))
            } else {
                (None, None)
            }
        }
        _ => (None, None),
    }
}

/// Rewrite an filter arrow function to use `expr`.
///
/// We use this to retain the type signature of an arrow function passed to
/// `ChiselCursor.filter()` but replace the expression in case we have
/// performed filter splitting.
///
/// For example, consider the following:
///
/// ```typescript
/// Person.cursor().filter(person => person.age < 4 && fetch("www.example.com"))
/// ```
///
/// the _arrow_ part is:
///
/// ```typescript
/// person => person.age < 4 && fetch("www.example.com")
/// ```
///
/// after filter splitting is performed we need two new arrows:
///
/// ```typescript
/// person => person.age < 4
/// ```
///
/// and
///
/// ```typescript
/// person => fetch("www.example.com")
/// ```
///
/// to do that, we basically substitute the original arrow with the
/// new split expressions. We know this is safe because the arrow
/// parameters remains the same, and we only split the expression (but
/// did not modify it) so the expressions will evaluate fine with the
/// original arrow parameters.
pub fn rewrite_filter_arrow(arrow: &ArrowExpr, expr: Expr) -> Expr {
    match &arrow.body {
        BlockStmtOrExpr::BlockStmt(block_stmt) => {
            assert_eq!(block_stmt.stmts.len(), 1);
            let return_stmt = match &block_stmt.stmts[0] {
                Stmt::Return(return_stmt) => return_stmt,
                _ => {
                    todo!();
                }
            };
            match &return_stmt.arg {
                Some(_expr) => {
                    let mut return_stmt = return_stmt.clone();
                    return_stmt.arg = Some(Box::new(expr));
                    let mut block_stmt = block_stmt.clone();
                    block_stmt.stmts[0] = Stmt::Return(return_stmt);
                    let mut arrow = arrow.clone();
                    arrow.body = BlockStmtOrExpr::BlockStmt(block_stmt);
                    Expr::Arrow(arrow)
                }
                None => {
                    todo!();
                }
            }
        }
        BlockStmtOrExpr::Expr(_) => {
            let mut arrow = arrow.clone();
            arrow.body = BlockStmtOrExpr::Expr(Box::new(expr));
            Expr::Arrow(arrow)
        }
    }
}
